package nl.ViewerServer.SessionManager;

import java.sql.SQLException;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.locks.ReentrantReadWriteLock;

import nl.ViewerServer.Database.SessionManagerDatabaseConnection;
import nl.ViewerServer.Exceptions.ParameterException;
import nl.ViewerServer.Exceptions.ServerException;
import nl.ViewerServer.Exceptions.UserRequestException;
import nl.ViewerServer.ServerManager.ServerManager;
import nl.ViewerServer.ServerManager.DataManager.Objects.Dataset;
import nl.ViewerServer.ServerManager.DataManager.Objects.JavaPythonUtil;
import nl.ViewerServer.ServerManager.DataManager.Objects.Modality;
import nl.ViewerServer.ServerManager.DataManager.Objects.Parameter;
import nl.ViewerServer.ServerManager.DataManager.Objects.Visualization;
import nl.ViewerServer.ServerManager.UserManager.Objects.User;
import nl.ViewerServer.SessionManager.Objects.Job;
import nl.ViewerServer.SessionManager.Objects.Session;
import nl.ViewerServer.Webservice.XMLUtil;

import org.apache.log4j.Logger;

public class SessionManager {
   private static Logger                       log                = Logger.getLogger( SessionManager.class );
   private static SessionManager               instance;
   private ConcurrentHashMap<Long, Session> sessions           = new ConcurrentHashMap<Long, Session>();
   public SessionManagerDatabaseConnection     database;

   public synchronized static SessionManager getInstance() {
      if( instance == null ) {
         try {
            instance = new SessionManager();
         }
         catch( Exception e ) {
            log.fatal( "Error while instantiating SessionManager", e );
            return null;
         }
      }
      return instance;
   }

   private SessionManager() throws Exception {
      log.info( "SessionManager started." );
      database = new SessionManagerDatabaseConnection();
   }

   public void shutdown() {
   }

   /**
    * The removeSession method physically removes the session from the sessions list.
    * 
    * @param session
    */
   public synchronized void removeSession( Session session ) {
      if( session != null )
         sessions.remove( session.sessionId );
   }

   public Session getSession( long sessionId ) {
      return sessions.get( sessionId );
   }

   public synchronized boolean userHasActiveInstance( int userId ) {
      // go through all sessions for this given user
      for( Session s : sessions.values() ) {
         if( s.userId == userId && s.isSessionActive() )
            return true;
      }
      return false;
   }

   public synchronized int getCountOfActiveInstancesForUser( int userId ) {
      HashSet<String> userAddresses = new HashSet<String>();

      // go through all sessions for this given user
      for( Session s : sessions.values() ) {
         if( s.userId == userId && s.isSessionActive() ) {
            // if our hashset contains the remote (ip) address we already counted the instance
            String address_hostname = s.remoteAddress + "_" + s.remoteHostname;
            if( userAddresses.contains( address_hostname ) == false ) {
               // a new client address found
               userAddresses.add( address_hostname );
            }
         }
      }

      return userAddresses.size();

   }

   public synchronized int getCountOfSessionsForUser( int userId ) {
      int count = 0;
      for( Session s : sessions.values() ) {
         if( s.isSessionActive() ) {
            if( s.userId == userId ) {
               count++;
            }
         }
      }
      return count;
   }

   /**
    * This method will create a server-based session
    * 
    * @param u
    * @param remoteAddress
    * @param hostname
    * @param modalityId
    * @param visualizationId
    * @param requestedParaViewThreadCount: -1 is have framework decide, all other values are still considered as requests, no guarantees can be given. 
    * 
    * @return the sessionId of the session that was created
    * @throws ServerException
    * @throws UserRequestException
    * @throws SQLException
    */
   public synchronized long createSession( User u, String remoteAddress, String hostname, int modalityId,
         int visualizationId, int requestedParaViewThreadCount ) throws ServerException, UserRequestException, SQLException {

      // check if the user has a restriction on the amount of instances
      if( u.maxInstances > 0 ) {
         // check if the user already has an instance
         if( userHasActiveInstance( u.userId ) == false ) {
            // if not, check if the user is allowed more instances
            int count = getCountOfActiveInstancesForUser( u.userId );
            if( u.maxInstances <= count ) {
               throw new UserRequestException(
                     "No more instances allowed to user (active: " + count + ", allowed: " + u.maxInstances + ")" );
            }
         }
      }

      // check if the user is allowed more sessions
      if( u.maxSessions > 0 ) {
         int count = getCountOfSessionsForUser( u.userId );
         if( u.maxSessions <= count ) {
            throw new UserRequestException(
                  "No more sessions allowed to user (active: " + count + ", allowed: " + u.maxSessions + ")" );
         }
      }

      Modality m = ServerManager.dataManager.getModality( modalityId );

      if( m == null || !ServerManager.userManager.userHasPrivilege( m.getModalityACL(), "SELECT", u ) )
         throw new UserRequestException( "Specified modality (" + modalityId + ") does not exist" );

      Dataset ds = ServerManager.dataManager.getDatasets().get( m.getDatasetId() );
      if( ds == null ) {
         // this is rather impossible
         throw new ServerException( "The dataset to which the specified modality belongs could not be found" );
      }

      // also check if the user is allowed to see the dataset, if not tell the user the modality does not exist
      if( !ServerManager.userManager.userHasPrivilege( ds.getDatasetACL(), "SELECT", u ) ) {
         throw new UserRequestException( "Specified modality (" + modalityId + ") does not exist" );
      }

      Visualization v = m.getVisualization( visualizationId );
      if( v == null || !ServerManager.userManager.userHasPrivilege( v.getVisualizationACL(), "SELECT", u ) ) {
         throw new UserRequestException(
               "Specified visualization (" + visualizationId + ") not available for modality " + modalityId );
      }

      // request a new sessionId
      long sessionId = database.getNextSessionId();
      
      Session s = new Session( sessionId, u, remoteAddress, hostname, m, v, requestedParaViewThreadCount );
      sessions.put( s.sessionId, s );
      return s.sessionId;
   }

   /**
    * Method to close a given client session. Additional parameter removeJobs is meant to allow scheduling of jobs where
    * the session will be closed, but its jobs will not be removed, to allow workers to continue on the jobs
    * 
    * @param u
    * @param sessionId
    * @param removeJobs
    * @throws UserRequestException
    */
   public synchronized void closeSession( User u, long sessionId, boolean removeJobs ) throws UserRequestException {
      Session s = getSession( sessionId );
      if( s == null )
         throw new UserRequestException( "Specified session does not exist" ); // don't delete non-existing session

      if( s.userId != u.userId )
         throw new UserRequestException( "Specified session is not owned by the user" );

      s.closeSession( removeJobs );

      if( removeJobs ) {
         ServerManager.jobManager.closeWorker( s );
      } else {
         // make sure the worker goes into 'batch mode': disable the timeout so the worker
         // will only live as long as there are jobs available for the session
         ServerManager.jobManager.disableWorkerTimeout( s );
      }
      sessions.remove( s );
   }

   public synchronized Job createJob( User u, long sessionId, String output, String[] inParameterNames,
         Object[] inJavaObjectParameterValue ) throws UserRequestException, SQLException {
      Session s = getSession( sessionId );

      if( s == null )
         throw new UserRequestException( "Specified session does not exist" );

      if( s.userId != u.userId )
         throw new UserRequestException( "Specified session is not owned by the user" );

      if( s.getStatus() == Session.Status.FAILED || s.getStatus() == Session.Status.CLOSED )
         throw new UserRequestException(
               "Specified session was either closed or failed and therefore does not accept jobs" );

      if( !s.checkIfOutputExists( output ) )
         throw new UserRequestException( "The specified output does not exist" );

      if( inParameterNames.length != inJavaObjectParameterValue.length ) {
         throw new UserRequestException( "The amount of parameter names does not match the amount of parameter values" );
      }

      // convert all input parameters
      for( int i = 0; i < inParameterNames.length; i++ ) {
         String pName = inParameterNames[i];
         Parameter p = s.getParameter( pName, output );
         if( p == null ) {
            throw new UserRequestException( "Parameter '" + pName + "' is not available in session" );
         }

         if( p.isUserModifiable() == false || p.isSessionParameter() || p.hasSetter() == false ) {
            throw new UserRequestException( "Parameter '" + pName + "' is not available or cannot be assigned" );
         }

         try {
            Object javaObject = JavaPythonUtil.convertJavaObjectToParameter( inJavaObjectParameterValue[i],
                  p.getParameterType(), p.getParameterLength() );
            inJavaObjectParameterValue[i] = javaObject;
         }
         catch( ParameterException pe ) {
            throw new UserRequestException(
                  "The input parameter value for '" + pName + "' (" + inJavaObjectParameterValue[i] + ") could not be converted: " + pe );
         }
      }

      // parameters OK

      // request a new jobId
      long jobId = database.getNextJobId();
      
      // create a new job based on the parameters specified and add it to the session
      Job j = new Job( jobId, s, output, inParameterNames, inJavaObjectParameterValue );
      s.addJob( j );

      // notify the jobManager that there is work
      ServerManager.jobManager.submitSession( s );

      return j;
   }

   public ArrayList<Parameter> getUserParametersForSession( User u, long sessionId, String outputName )
         throws UserRequestException {
      Session s = getSession( sessionId );

      if( s == null )
         throw new UserRequestException( "Specified session does not exist" );

      if( s.userId != u.userId )
         throw new UserRequestException( "Specified session is not owned by the user" );

      if( s.isOutputValid( outputName ) == false )
         throw new UserRequestException( "Output '" + outputName + "' is not valid for the specified session" );

      HashMap<String, Parameter> params = s.getAllVisualizationParametersForOutput( outputName );

      ArrayList<Parameter> paramList = new ArrayList<Parameter>();
      // now remove all non-userModifiable and non-userViewable parameters
      for( Parameter p : params.values() ) {
         if( p.isUserModifiable() || p.isUserViewable() ) {
            paramList.add( p );
         }
      }
      params = null;
      return paramList;
   }

   /**
    * Get the status of all session and jobs for a user that are within one instance (i.e. same remoteAddress).
    * 
    * @param u
    * @param remoteAddress
    * @param remoteHostname
    * @return
    */
   public String getStatus( User u, String remoteAddress, String remoteHostname ) {
      int sessionsChecked = 0;
      ArrayList<Session> alteredSessions = new ArrayList<Session>();
      // go through all sessions specified
      for( Session s : sessions.values() ) {
         // if the session belongs to the authorized user
         if( s.userId == u.userId && s.remoteAddress.equals( remoteAddress ) && s.remoteHostname.equals( remoteHostname ) ) {
            sessionsChecked++;
            // and check whether the session has had its status altered
            if( s.getAndResetSessionAltered() ) {
               // add it to the list

               alteredSessions.add( s );
            }
         }
      }

      if( sessionsChecked > 0 ) {
         if( alteredSessions.size() == 0 ) {
            // Special case to signal that there were no altered sessions
            // Don't return null because null means no (valid) sessionIds were specified
            // and then the webservice should return immediately

            return "";
         }

         // each method in sessions/jobs that alter the state of this status overview must
         // have requested either a read- or a writelock. If we writeLock all alteredSessions we are sure not to have a
         // concurrent modification exception in the serialization of the sessions/jobs into xml.
         //

         ArrayList<ReentrantReadWriteLock.WriteLock> locks = new ArrayList<ReentrantReadWriteLock.WriteLock>();
         String xml = null;
         try {

            for( Session s : alteredSessions ) {
               s.writeLock.lock();
               locks.add( s.writeLock );
            }

            xml = XMLUtil.SessionArrayListToXML( alteredSessions );
         }
         finally {
            // be sure to unlock any locks that we have have requested
            for( ReentrantReadWriteLock.WriteLock l : locks ) {
               l.unlock();
            }
         }

         for( Session s : alteredSessions ) {
            s.clearAlteredJobs();
         }

         return xml;
      }
      return null;
   }

}
